
package stratego.java.ssm;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.event.InputEvent;
import java.util.HashMap;
import java.util.Random;

import javax.swing.JFrame;
import javax.swing.JOptionPane;

import stratego.java.ssm.gui.BoardEvent;
import stratego.java.ssm.gui.BoardEventListener;
import stratego.java.ssm.gui.GameCanvas;
import stratego.java.ssm.gui.PieceSelector;
import stratego.java.ssm.piece.EmptyPiece;
import stratego.java.ssm.piece.Piece;

/**
 * <code>public class OfflineGame</code>
 * <p>
 * This class handles an offline version of the game. It initializes the canvas, and handles the
 * game logic.
 * </p>
 * 
 * @author Mark Aligbe
 * 
 */
public class OfflineGame implements BoardEventListener
{
	// The main frame (from Stratego)
	private JFrame	      frame;
	// The rendering canvas (from Stratego)
	private GameCanvas	  canvas;
	// For interacting, limitedly, with the render thread.
	private Thread	      renderThread;
	// The board object. Allows more complex control than in GameCanvas.
	private Board	      board;
	// The selector object for selecting pieces.
	private PieceSelector	selector;
	// A lock used to indicate that selection is in progress
	private Object	      lock;
	// The Array of Players
	private Player[]	  players;
	// The winner of the match. Due to the mutli-threaded nature of the code, an instance variable
	// is needed
	private Player	      winner;
	// A queue holding the currently clicked pieces, for deciding if something needs to be done
	private Piece[]	      pieceQueue;
	// A queue for holding the points of the currently clicked pieces.
	private Point[]	      pointQueue;
	// Indicates which player is currently moving.
	private Player	      currentPlayer;
	
	/**
	 * <code>public OfflineGame(JFrame frame, GameCanvas canvas)</code>
	 * <p>
	 * Starts an offline game. Begins the rendering thread and sets up the board for both players.
	 * </p>
	 * 
	 * @param frame
	 *            - The main frame of the Stratego game. Contains the GameCanvas object
	 * @param canvas
	 *            - The GameCanvas of the Stratego game.
	 */
	public OfflineGame(JFrame frame, GameCanvas canvas)
	{
		this.frame = frame;
		this.canvas = canvas;
		
		// // //////////////////////////////////////////////////////Custom board mode is a no go
		// // Get the board type the player wishes to use
		// String[] options = {"Classic Board", "Custom Board"};
		// int boardType =
		// JOptionPane.showOptionDialog(this.frame, "What board type do you want to play on?",
		// "Choose Board Type", JOptionPane.DEFAULT_OPTION,
		// JOptionPane.QUESTION_MESSAGE, null, options, options[0]);
		// // //////////////////////////////////////////////////////
		
		// Register this object in the GameCanvas object
		this.canvas.registerBoardListener(this);
		// Initialize the appropriate kind of board
		int boardSize = 40;
		int boardType = 0;
		if (boardType == -1)
			System.exit(0);
		else if (boardType == 0)
			board = new Board(Board.CLASSIC, 10);
		// Ask the user what board size to use.
		else if (boardType == 1)
		{
			boardSize = UserInput.getDimensions(frame);
			board = new Board(Board.CUSTOM, boardSize);
		}
		canvas.addBoard(board);
		renderThread = new Thread(this.canvas);
		// Have the players set up their board
		JOptionPane.showMessageDialog(frame, "Player 1 - place your pieces.", "Piece Placement",
		        JOptionPane.INFORMATION_MESSAGE);
		lock = new Object();
		renderThread.start();
		// Set up the players
		players = new Player[2];
		players[0] = new Player(1);
		// Set up the board and wait for the PieceSelector to finish
		setupBoard(new Dimension(256, 512), boardSize, 1);
		wait(lock);
		// Flip the pieces belonging to this player so the other player can pick (and wait. It's a
		// common theme)
		canvas.flipPieces(players[0].getPieceLocations(), lock);
		wait(lock);
		// Show a dialog option to indicate to the players that the other player is selecting
		JOptionPane.showMessageDialog(frame, "Player 2 - place your pieces.", "Piece Placement",
		        JOptionPane.INFORMATION_MESSAGE);
		// Set up the other player
		players[1] = new Player(2);
		setupBoard(new Dimension(256, 512), boardSize, 2);
		wait(lock);
		// Move the gamecanvas out of the rendering pipeline
		canvas.isSelecting.set(false);
		canvas.flipPieces(players[1].getPieceLocations(), lock);
		wait(lock);
		pieceQueue = new Piece[2];
		// Makes the purpose of pointQueue more explicit
		pointQueue = new Point[pieceQueue.length];
		runGame();
	}
	
	/**
	 * <code>private void runGame()</code>
	 * <p>
	 * Starts the game logic. Prompts each player to make a move, modifies the board accordingly,
	 * and handles launchin all subsequent animations.
	 * </p>
	 */
	private void runGame()
	{
		// Anonymous object. We don't need to take up anymore memory than we already are. Randomly
		// assign a player to be p1.
		Player p1 = players[(new Random()).nextInt(2)];
		// The other player is which ever one p1 is not.
		Player p2 = null;
		if (p1.getNum() == 1)
			p2 = players[1];
		else
			p2 = players[0];
		// A sanity check as well as prompts the players that the game is beginning
		JOptionPane.showMessageDialog(frame, "Player " + p1.getNum()
		        + " has been randomly selected to go first. Player " + p2.getNum()
		        + " will go second.", "Coin Toss", JOptionPane.INFORMATION_MESSAGE);
		canvas.inGame.set(true);
		// Loop until winner has been assigned (by executeAction)
		while (winner == null)
		{
			// Flips p1's pieces face up
			canvas.flipPieces(p1.getPieceLocations(), lock);
			wait(lock);
			// Used by executeAction. Indicates for this object who p1 is.
			currentPlayer = p1;
			int good = 0;
			do
			{
				// This wait is broken by a fired BoardClickedEvent. As the next dialog shows, the
				// game waits for the player to make a move and then asks the player to confirm. If
				// they say "yay," then we continue, otherwise we clear the current selection and
				// wait again.
				wait(lock);
				good =
				        JOptionPane.showConfirmDialog(frame, "Execute move and end turn?",
				                "Make Move", JOptionPane.YES_NO_OPTION);
				if (good != 0)
				{
					for (int i = 0; i < pieceQueue.length; i++)
					{
						pieceQueue[i] = null;
						pointQueue[i] = null;
					}
					canvas.clearHighlightLocation();
				}
			}
			while (good != 0);
			// Clear the highlightedPieces
			canvas.clearHighlightLocation();
			// Execute whatever action is necessary.
			executeAction(p1, p2, pieceQueue, pointQueue);
			// Clear the selection after we're done
			for (int i = 0; i < pieceQueue.length; i++)
			{
				pieceQueue[i] = null;
				pointQueue[i] = null;
			}
			// Check to see if the last move was a finishing movie
			if (winner != null)
			{
				canvas.flipPieces(p2.getPieceLocations(), lock);
				wait(lock);
				break;
			}
			// Flip down the last player's pieces
			canvas.flipPieces(p1.getPieceLocations(), lock);
			wait(lock);
			// Start the second player's turn, and do essentially the same as above.
			JOptionPane.showMessageDialog(frame, "Player " + p2.getNum() + "'s turn.",
			        "Turn Chage", JOptionPane.INFORMATION_MESSAGE);
			canvas.flipPieces(p2.getPieceLocations(), lock);
			wait(lock);
			currentPlayer = p2;
			good = 0;
			do
			{
				wait(lock);
				good =
				        JOptionPane.showConfirmDialog(frame, "Execute move and end turn?",
				                "Make Move", JOptionPane.YES_NO_OPTION);
				if (good != 0)
				{
					for (int i = 0; i < pieceQueue.length; i++)
					{
						pieceQueue[i] = null;
						pointQueue[i] = null;
					}
					canvas.clearHighlightLocation();
				}
			}
			while (good != 0);
			canvas.clearHighlightLocation();
			executeAction(p2, p1, pieceQueue, pointQueue);
			for (int i = 0; i < pieceQueue.length; i++)
			{
				pieceQueue[i] = null;
				pointQueue[i] = null;
			}
			// Check to see if the last move was a finishing movie
			if (winner != null)
			{
				canvas.flipPieces(p1.getPieceLocations(), lock);
				wait(lock);
				break;
			}
			canvas.flipPieces(p2.getPieceLocations(), lock);
			wait(lock);
			JOptionPane.showMessageDialog(frame, "Player " + p1.getNum() + "'s turn.",
			        "Turn Chage", JOptionPane.INFORMATION_MESSAGE);
		}
		JOptionPane.showMessageDialog(frame, "Player " + winner.getNum() + " wins!", "Victory!",
		        JOptionPane.INFORMATION_MESSAGE);
	}
	
	/**
	 * <code>private void executeAction(Player actor, Player reciever, Piece[] pieceQueue, Point[] pointQueue)</code>
	 * <p>
	 * Executes a set of actions based on the pieces and their locations. It will either move a
	 * piece to an empty location, destroy a piece and move the surviving piece to its old location,
	 * destroy two pieces, or declare a winner.
	 * </p>
	 * 
	 * @param actor
	 *            - The player who case the first stone.
	 * @param reciever
	 *            - The player who must turn the other cheek.
	 * @param pieceQueue
	 *            - The pieces involved in the conflict.
	 * @param pointQueue
	 *            - The locations of the pieces involved in the conflict.
	 */
	private void
	        executeAction(Player actor, Player reciever, Piece[] pieceQueue, Point[] pointQueue)
	{
		// Assign the pieces their corresponding role. The attacking piece belongs to the acting
		// Player.
		Piece att = pieceQueue[0];
		Piece def = pieceQueue[1];
		Point attLoc = pointQueue[0];
		Point defLoc = pointQueue[1];
		// For empty spaces, simply move the other piece.
		if (def.isEmpty())
		{
			// Tell the player that their piece is now in this location
			actor.updatePiece(att, attLoc, defLoc);
			// Animate the move
			canvas.movePiece(att, def, attLoc, defLoc, lock);
			wait(lock);
			// No need to make new objects, just swap the references
			board.getBoard()[defLoc.x][defLoc.y] = att;
			board.getBoard()[attLoc.x][attLoc.y] = def;
			return;
		}
		// Flip over the defending piece
		HashMap<Point, Piece> flip = new HashMap<Point, Piece>();
		flip.put(defLoc, def);
		canvas.flipPieces(flip, lock);
		wait(lock);
		// For non empty spaces (i.e. two pieces, use the piece comparator to get the winning piece)
		Piece surv = PieceComparator.comparePiece(att, def);
		Piece loser = null;
		Point losLoc = null;
		Point winLoc = null;
		Player win = null;
		Player los = null;
		// The comment explaining this would be too long, rather I hope the variable names are clear
		// enough. As far as why this works, one must understand the nature of Java. Using Object ==
		// Object rather than Object.equals(Object) compares the references, not the contents. In
		// completely correct terms: it compares the memory location of the two objects rather than
		// executing a set of algorithms defined by the .equals() method.
		if (surv == att)
		{
			win = actor;
			los = reciever;
			loser = def;
			winLoc = attLoc;
			losLoc = defLoc;
		}
		else
		{
			win = reciever;
			los = actor;
			loser = att;
			winLoc = defLoc;
			losLoc = attLoc;
		}
		// The piece to destroy
		HashMap<Point, Piece> pieces = new HashMap<Point, Piece>();
		// If the losing piece is the flag, then we have a victor
		if (def.isFlag())
		{
			winner = win;
			pieces.put(defLoc, def);
			canvas.flipPieces(pieces, lock);
			wait(lock);
			return;
		}
		// If surv is null, then that means both pieces are destroyed
		if (surv == null)
		{
			// Indicate to both players to remove the piece at the specified location
			actor.decrementPiece(att, attLoc);
			reciever.decrementPiece(def, defLoc);
			// Animate the destruction of these pieces
			pieces.put(attLoc, att);
			pieces.put(defLoc, def);
			canvas.destroyPieces(pieces, lock);
			wait(lock);
			// We have to make two new empty pieces
			board.getBoard()[attLoc.x][attLoc.y] = new EmptyPiece();
			board.getBoard()[defLoc.x][defLoc.y] = new EmptyPiece();
			// Don't execute any of the further logic, as it's already been taken care of here
			return;
		}
		// The losing piece is to be destroyed
		pieces.put(losLoc, loser);
		// If the winner is a bomb, then we have to destroy both pieces
		if (surv.isBomb())
			pieces.put(winLoc, surv);
		canvas.destroyPieces(pieces, lock);
		wait(lock);
		// Update the player's boards, and the main board
		los.decrementPiece(loser, losLoc);
		Piece empty = new EmptyPiece();
		board.getBoard()[losLoc.x][losLoc.y] = empty;
		// If the winner was the attacker, then move the attacker's piece to the now vacant loser's
		// square
		if (win == actor)
		{
			win.updatePiece(surv, winLoc, losLoc);
			canvas.movePiece(surv, empty, winLoc, losLoc, lock);
			wait(lock);
			board.getBoard()[losLoc.x][losLoc.y] = surv;
			board.getBoard()[winLoc.x][winLoc.y] = empty;
		}
		// Otherwise, flip back over the defender's piece
		else
		{
			// If the defender's piece was a bomb, it too should be destroyed
			if (surv.isBomb())
			{
				win.decrementPiece(surv, winLoc);
				board.getBoard()[winLoc.x][winLoc.y] = new EmptyPiece();
			}
			// If not, just flip over the normal piece.
			else
			{
				canvas.flipPieces(flip, lock);
				wait(lock);
			}
		}
	}
	
	/**
	 * <code>private void wait(Object lock)</code>
	 * <p>
	 * Causes the thread this object is currently working on until the specified object is notified.
	 * Useful for pausing game execution until the user performs some sort (or series) of input.
	 * </p>
	 * 
	 * @param lock
	 *            - The object to wait on. Waits indefinitely.
	 */
	private void wait(Object lock)
	{
		synchronized (lock)
		{
			try
			{
				lock.wait();
			}
			catch (InterruptedException e)
			{
				e.printStackTrace();
			}
		}
	}
	
	/**
	 * <code>private void setupBoard(Dimension dimension, int boardSize, int player)</code>
	 * <p>
	 * Sets up the board for a player. Makes a new window to allow a player to place their pieces.
	 * </p>
	 * 
	 * @param dimension
	 *            - The size of the PieceSelector window
	 * @param boardSize
	 *            - The size of the board (e.g. 10 for 10x10)
	 * @param player
	 *            - The player number to set up. Determines the color gradient of the pieces.
	 */
	private void setupBoard(Dimension dimension, int boardSize, int player)
	{
		Point location = new Point(frame.getWidth(), frame.getLocation().y);
		selector =
		        new PieceSelector(dimension, boardSize, player, location, lock, this, board,
		                canvas, frame);
		selector.setPlayerPieceLocations(players[player - 1].getPieceLocations());
		canvas.setSelector(selector);
		canvas.isSelecting.set(true);
		selector.showFrame();
	}
	
	@Override
	/**Allows the game to respond to MouseEvents fired on GameCanvas*/
	public void boardClicked(BoardEvent e)
	{
		// If we are currently in selection mode, then place or remove a piece depending on the
		// button
		if (canvas.isSelecting.get() && selector != null)
		{
			// If the click isn't in the approved region, then don't do anything
			if (!checkRegion(selector.getPlayer(), e.getY()))
				return;
			// Get the piece at the current location
			Piece current = board.getBoard()[e.getX()][e.getY()];
			// If it's empty, then add the desired piece to the location, and inform all relevant
			// objects of this
			if (current.isEmpty() && e.getButton() == InputEvent.BUTTON1_MASK)
			{
				board.getBoard()[e.getX()][e.getY()] = e.getPiece();
				players[selector.getPlayer() - 1].addPiece(e.getPiece(),
				        new Point(e.getX(), e.getY()));
				selector.decrementPiece(e.getPiece());
			}
			// If the player right clicked, then they want to remove the piece at that location (but
			// we can only do that for non-empty blocks and not BlockPieces.
			else if (!current.isEmpty() && !current.isBlock()
			        && e.getButton() == InputEvent.BUTTON3_MASK)
			{
				// Inform the relevant objects that the piece at the current location is to be
				// removed
				Piece oldPiece = board.getBoard()[e.getX()][e.getY()];
				board.getBoard()[e.getX()][e.getY()] = new EmptyPiece();
				players[selector.getPlayer() - 1].decrementPiece(oldPiece,
				        new Point(e.getX(), e.getY()));
				selector.incrementPiece(oldPiece);
			}
		}
		// Logic for when a board is clicked and we are in game.
		if (canvas.inGame.get() && e.getButton() == InputEvent.BUTTON1_MASK)
		{
			Piece p = e.getPiece();
			Point loc = new Point(e.getX(), e.getY());
			// Getting the piece of the player
			if (currentPlayer.getPieceLocations().containsKey(loc))
			{
				// Making sure not to add unmovable pieces
				if (!p.isBomb() && !p.isFlag())
				{
					canvas.clearHighlightLocation();
					pieceQueue[0] = p;
					pointQueue[0] = loc;
					canvas.highlightLocation(loc, new Color(0, 200, 0, 128));
				}
			}
			// It would be weird for this code to initiate an attack if the player selected an
			// enemy piece first
			else if (pieceQueue[0] == null)
				return;
			// Getting the target square for that piece
			else
			{ // Check to make sure the move is legal
				if (p.isBlock())
					return;
				// Check to make sure the other click location is in a line, is 1 grid away if
				// it is a regular piece, and doesn't traverse any illegal squares if it is a
				// scout
				if (!checkDistance(pieceQueue[0], p, pointQueue[0], loc))
					return;
				pieceQueue[1] = p;
				pointQueue[1] = loc;
				canvas.highlightLocation(loc, new Color(200, 0, 0, 128));
				// We should notify the game thread
				synchronized (lock)
				{
					lock.notify();
				}
			}
		}
	}
	
	/**
	 * <code>private boolean checkDistance(Piece pSrc, Piece pDest, Point src, Point dest)</code>
	 * <p>
	 * Checks the line between two pieces and makes sure a move is allowable. A move is allowable if
	 * the destination square is not a diagonal location or BlockPiece, and it is one square away
	 * from the source piece (or there is nothing between it and the other piece if it is a scout).
	 * </p>
	 * 
	 * @param pSrc
	 *            - The piece that desires to move.
	 * @param pDest
	 *            - The piece at the location the moving piece wants to go to.
	 * @param src
	 *            - The location of the piece that desires to move.
	 * @param dest
	 *            - The location that the piece wants to move to.
	 * @return True if the move is allowable. False otherwise.
	 */
	private boolean checkDistance(Piece pSrc, Piece pDest, Point src, Point dest)
	{
		double distance =
		        Math.sqrt(Math.pow(src.getX() - dest.getX(), 2.0)
		                + Math.pow(src.getY() - dest.getY(), 2.0));
		// Check to see if it has a fractional part. If it does, then the other location is a
		// diagonal location
		if (Math.floor(distance) - distance != 0.0)
			return false;
		// If the distance is greater than 1, and it's not a scout, then that's an illegal move
		if (distance > 1.0 && !pSrc.isScout())
			return false;
		// If it is a scout, get the direction of the move and make sure there's no other piece in
		// the way
		else
		{
			// The src and dest are horizontal
			if (src.x - dest.x != 0)
			{
				int startX = 0, endX = 0;
				if (src.x > dest.x)
				{
					startX = src.x;
					endX = dest.x;
				}
				else
				{
					endX = src.x;
					startX = dest.x;
				}
				// Don't check the starting grid (it could be a piece or empty, doesn't matter to
				// us)
				startX--;
				// Don't check the same grid as the original piece either
				while (startX > endX)
				{
					// The src and dest y are the same
					if (!board.getBoard()[startX][src.y].isEmpty())
						return false;
					startX--;
				}
			}
			// The src and dest are vertical
			else
			{
				int startY = 0, endY = 0;
				if (src.y > dest.y)
				{
					startY = src.y;
					endY = dest.y;
				}
				else
				{
					endY = src.y;
					startY = dest.y;
				}
				// Don't check the starting grid (it could be a piece or empty, doesn't matter to
				// us)
				startY--;
				// Don't check the same grid as the original piece either
				while (startY > endY)
				{
					// The src and dest x are the same
					if (!board.getBoard()[src.x][startY].isEmpty())
						return false;
					startY--;
				}
			}
		}
		return true;
	}
	
	/**
	 * <code>private boolean checkRegion(int player, int x, int y)</code>
	 * <p>
	 * Checks to make sure the player's piece is in the approved region. It is understood that the
	 * entire width of the board is acceptable.
	 * </p>
	 * 
	 * @param player
	 *            - An int representing which player is placing their piece.
	 * @param y
	 *            - An int to represent the y-grid location that the piece wants to be placed at
	 * @return True if the piece belongs in this region. False otherwise.
	 */
	private boolean checkRegion(int player, int y)
	{
		if (player == 1)
		{
			if (y < board.getBoard().length / 2 + 1)
				return false;
		}
		else
		{
			if (y > board.getBoard().length / 2 - 1)
				return false;
		}
		return true;
	}
}
